/*
 *    Copyright 2008-2009 The PoCo Team
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package com.asemantics.poco.serializer;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.Writer;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.commons.beanutils.ConversionException;
import org.apache.commons.beanutils.ConvertUtils;

import com.asemantics.poco.Account;
import com.asemantics.poco.Activity;
import com.asemantics.poco.Address;
import com.asemantics.poco.BodyType;
import com.asemantics.poco.Email;
import com.asemantics.poco.Entry;
import com.asemantics.poco.IM;
import com.asemantics.poco.MediaItem;
import com.asemantics.poco.Name;
import com.asemantics.poco.Organization;
import com.asemantics.poco.PhoneNumber;
import com.asemantics.poco.Photo;
import com.asemantics.poco.Response;
import com.asemantics.poco.Url;

/**
 * 
 * @author Simone Tripodi
 * @version $Id: AbstractPoCoSerializer.java 272 2009-12-21 12:01:38Z simone.tripodi@gmail.com $
 */
public abstract class AbstractPoCoSerializer {

    private final Logger logger = Logger.getLogger(this.getClass().getName());

    private final Map<Class<?>, BeanInfo> beanInfoMap = new HashMap<Class<?>, BeanInfo>();

    /**
     * 
     * @param writer
     */
    public AbstractPoCoSerializer(Writer writer) {
        this(writer, false);
    }

    /**
     * 
     * @param outputStream
     * @param prettyPrint
     */
    public AbstractPoCoSerializer(Writer writer, boolean prettyPrint) {
        this.init(writer, prettyPrint);

        for (Class<?> clazz : new Class<?>[] { Account.class,
                Address.class,
                Activity.class,
                BodyType.class,
                Email.class,
                Entry.class,
                IM.class,
                MediaItem.class,
                Name.class,
                Organization.class,
                PhoneNumber.class,
                Photo.class,
                Response.class,
                Url.class }) {
            try {
                BeanInfo beanInfo = Introspector.getBeanInfo(clazz);
                this.beanInfoMap.put(clazz, beanInfo);
            } catch (IntrospectionException e) {
                // can this exception be catched quietly?
            }
        }

        if (this.logger.isLoggable(Level.FINE)) {
            this.logger.fine("beanInfoMap initialized with values: "
                    + this.beanInfoMap);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void serialize(Response response) throws PoCoSerializerException {
        this.startDocument();
        this.visit(response);
        this.endDocument();
    }

    /**
     * 
     * @param value
     * @throws PoCoSerializerException
     */
    private void visit(final Object value) throws PoCoSerializerException {
        if (value instanceof String || value instanceof Number || value instanceof Boolean || value instanceof Enum<?>) {
            String text = null;
            boolean literal = (value instanceof Number || value instanceof Boolean);
            try {
                text = (String) ConvertUtils.convert(value, String.class);
            } catch (ConversionException e) {
                throw new PoCoSerializerException(e);
            }

            this.value(text, literal);
        } else {
            this.startObject();

            Class<?> type = value.getClass();
            BeanInfo beanInfo = null;
            if (!this.beanInfoMap.containsKey(type)) {
                try {
                    beanInfo = Introspector.getBeanInfo(type);
                    this.beanInfoMap.put(type, beanInfo);
                } catch (Exception e) {
                    throw new PoCoSerializerException("Impossible to inrospect object ["
                            + value
                            + "] of "
                            + type, e);
                }
            } else {
                beanInfo = this.beanInfoMap.get(value.getClass());
            }
            for (PropertyDescriptor pd : beanInfo.getPropertyDescriptors()) {
                Class<?> clazz = pd.getPropertyType();

                if (clazz != Class.class) {
                    String propertyName = pd.getName();

                    Object child = null;
                    try {
                        Method readMethod = pd.getReadMethod();
                        if( readMethod == null ) {
                            throw new PoCoSerializerException("Cannot find readMethod for property '" + pd.getName() + "'");
                        }
                        child = readMethod.invoke(value);
                    } catch (IllegalArgumentException e) {
                        // can this exception be catched quietly?
                    } catch (IllegalAccessException e) {
                        // can this exception be catched quietly?
                    } catch (InvocationTargetException e) {
                        // can this exception be catched quietly?
                    }

                    if (child != null && !Boolean.FALSE.equals(child)) {
                        if (List.class.isAssignableFrom(clazz)) {
                            List<?> list = (List<?>) child;

                            if (!list.isEmpty()) {
                                this.startElement(propertyName);
                                this.startList();
                                for (Object o : list) {
                                    this.visit(o);
                                }
                                this.endList();
                                this.endElement();
                            }
                        } else {
                            this.startElement(propertyName);
                            this.visit(child);
                            this.endElement();
                        }
                    }
                }
            }

            this.endObject();
        }
    }

    protected abstract void init(Writer writer, boolean prettyPrint);

    protected abstract void startDocument() throws PoCoSerializerException;

    protected abstract void startElement(String name) throws PoCoSerializerException;

    protected abstract void endElement() throws PoCoSerializerException;
    
    protected abstract void value(String text, boolean literal) throws PoCoSerializerException;

    protected abstract void startObject() throws PoCoSerializerException;

    protected abstract void endObject() throws PoCoSerializerException;

    protected abstract void startList() throws PoCoSerializerException;

    protected abstract void endList() throws PoCoSerializerException;

    protected abstract void endDocument() throws PoCoSerializerException;

}
